
// import * as d3 from "../lib/d3.js";
import * as d3 from "d3";
import {findIndex, findLastIndex} from "underscore";
import {floorMod} from "../util/math.js";

// UNDONE: this module contains competing definitions of focusable

/**
 * d3 selector for elements that can possibly receive browser focus. Note that some elements may not actually be
 * focusable due to hidden parents, invisibility, inertness, etc.
 */
export const focusableElements = d3.selectorAll("a[href], button, [tabindex]");

/**
 * @param {HTMLElement} node
 * @returns {boolean} true when the element is focusable
 */
export function isFocusable(node) {
    // For now, "focusable" means an element that is not hidden, inert, or cloaked, including ancestors
    if ((node ?? null) === null) {
        return false;
    }
    for (let cursor = node; cursor !== null; cursor = (cursor.parentElement ?? null)) {
        if (cursor.hidden || cursor.hasAttribute("inert") || cursor.classList.contains("cloaked")) {
            return false;
        }
    }
    return true;
}

/**
 * @param {HTMLElement[]} nodes
 * @returns {HTMLElement} the first element in the array that is focusable, or undefined if none
 */
export function firstFocusable(nodes) {
    return nodes[findIndex(nodes, isFocusable)];
}

/**
 * @param {HTMLElement[]} nodes
 * @param {number} start
 * @param {number} step
 * @returns {HTMLElement} the next element in the ring that is focusable, or undefined if none
 */
export function nextFocusable(nodes, start, step) {
    const n = nodes.length;
    start = floorMod(start, n);
    for (let next = floorMod(start + step, n); next !== start; next = floorMod(next + step, n)) {
        if (isFocusable(nodes[next])) {
            return nodes[next];
        }
    }
}

/**
 * @param {HTMLElement[]} nodes
 * @returns {HTMLElement} the last element in the array that is focusable, or undefined if none
 */
export function lastFocusable(nodes) {
    return nodes[findLastIndex(nodes, isFocusable)];
}
